-- Copyright 2012 Nicolas BARBOTIN
-- This file is part of PlayeRanks.
-- 
-- PlayeRanks is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- 
-- PlayeRanks is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with PlayeRanks.  If not, see <http://www.gnu.org/licenses/>.

if SERVER then
	pranks = {}
	pranks.ranks = {}

	-- PlayeRanks version here
	pranks.version = "0.1"

	util.AddNetworkString("PlayeRanks")

	local ranks = file.Read("Ranks.txt", "DATA")
	if !ranks then
		MsgN("[ERROR] PlayeRanks: Failed to load ranks config file !!!")
	else
		local parse = string.Explode("\n", ranks)
		local cRank = -1
		local sealed = false

		for _, line in pairs(parse) do
			line = string.Trim(line)

			if string.len(line) > 0 and line[1] != "#" and line[1] != "@" then
				if !sealed then
					MsgN("[ERROR] PlayeRanks: Failed to parse ranks config file: Trying to start a rank before the version seal at line" .. tostring(_))
					break
				elseif line[1] == "[" then -- Improves parse speed
					local rsID, rName = string.match(line, "%[%s*([0-9]+)%s*,%s*([a-zA-Z0-9]+)%s*%]")

					if !rsID or !rName then
						MsgN("[ERROR] PlayeRanks: Failed to parse ranks config file: Invalid rank define statement at line " .. tostring(_))
						break
					else
						if cRank > 0 and pranks.ranks[cRank].rights[string.len(pranks.ranks[cRank].rights)] == ";" then
							-- Remove the last ; if needed
							pranks.ranks[cRank].rights = string.sub(pranks.ranks[cRank].rights, 1, string.len(pranks.ranks[cRank].rights) - 1)
						end

						rID = tonumber(rsID)
						pranks.ranks[rID] = {}
						pranks.ranks[rID].name = rName
						pranks.ranks[rID].colorRed = 0
						pranks.ranks[rID].colorGreen = 0
						pranks.ranks[rID].colorBlue = 255
						pranks.ranks[rID].rights = ""
						cRank = rID
					end
				elseif cRank < 0 then
					MsgN("[ERROR] PlayeRanks: Failed to parse ranks config file: Property set before rank start at line " .. tostring(_))
					break
				else
					local property, value = string.match(line, "([a-zA-Z]+)%s*=%s*([a-zA-Z0-9%.%*_;]+)")

					if !property or !value then
						MsgN("[ERROR] PlayeRanks: Failed to parse ranks config file: Invalid property assignement statement a line " .. tostring(_))
						break
					else
						-- Parse property and assign a value to it
						property = string.lower(property)

						if property == "colorr" then
							pranks.ranks[cRank].colorRed = tonumber(value)
						elseif property == "colorg" then
							pranks.ranks[cRank].colorGreen = tonumber(value)
						elseif property == "colorb" then
							pranks.ranks[cRank].colorBlue = tonumber(value)
						elseif property == "rights" then
							pranks.ranks[cRank].rights = pranks.ranks[cRank].rights .. value .. ";"
						else
							MsgN("[ERROR] PlayeRanks: Failed to parse ranks config file: Invalid property: " .. property .. " at line " .. tostring(_))
							break
						end
					end
				end
			elseif line[1] and line[1] == "@" then
				if cRank != -1 then
					MsgN("[ERROR] PlayeRanks: Failed to parse ranks config file: Rank started before seal at line " .. tostring(_))
					break
				elseif sealed then
					MsgN("[ERROR] PlayeRanks: Failed to parse ranks config file: Already sealed (line " .. tostring(_) .. ") !")
					break
				else
					local version = string.match(line, "@version%s+([0-9%.]+)")

					if !version then
						MsgN("[ERROR] PlayeRanks: Failed to parse ranks config file: Invalid version seal at line " .. tostring(_) .. " !")
						break
					elseif version != pranks.version then
						MsgN("[ERROR] PlayeRanks: Ranks config file is NOT up to date (line " .. tostring(_) .. ") ! File version : " .. version .. ", PlayeRanks version : " .. pranks.version)
						break
					else
						sealed = true
					end
				end
			end
		end

		if cRank > 0 then
			local lastEntry = pranks.ranks[table.Count(pranks.ranks)]

			if lastEntry and lastEntry.rights[string.len(lastEntry.rights)] == ";" then
				-- Remove the last ; if needed
				pranks.ranks[table.Count(pranks.ranks)].rights = string.sub(lastEntry.rights, 1, string.len(lastEntry.rights) - 1)
			end
		end
	end

	CreateConVar("pr_default_rank", 1)
	CreateConVar("pr_kickerror", 1)

	-- Setups PRank. Call it when you installed the addon
	-- Returns true if the setup was successfull, false if it wasn't (Boolean)
	function pranks.Setup()
		local query = sql.Query("CREATE TABLE IF NOT EXISTS playerank (id INTEGER PRIMARY KEY ASC AUTOINCREMENT, steam TEXT, rank INTEGER)")
		if query == false then
			MsgN("[ERROR] PlayeRanks: Failed to create the main table: " .. sql.LastError())
			return false
		end

		MsgN("[INFO] PlayeRanks: Setup was successfull !")
		return true
	end

	-- Get a player's rank
	-- playr: The target player (Player)
	-- Returns -2 if there was an error, -1 if the player does not exists or the rank id if it exists (Integer)
	function pranks.RankOf(playr)
		local query = sql.Query("SELECT rank FROM playerank WHERE steam='" .. sql.SQLStr(playr:SteamID()) .. "'")
		if query == false then
			MsgN("[ERROR] PlayeRanks: Failed to find a player's rank: " .. sql.LastError())
			return -2
		elseif !query or !query[1] or !query[1]["rank"] then
			return -1
		else
			return tonumber(query[1]["rank"])
		end
	end

	-- Changes the rank of a player
	-- playr: The target player (Player)
	-- rank: The rank ID (Integer)
	-- Returns false if the operation failed, true if it was successfull (Boolean)
	function pranks.SetRank(playr, rank)
		if !ranks[rank] then
			MsgN("[ERROR] PlayeRanks: Rank ID " .. tostring(rank) .. " does not exists !")
			return false
		end

		local cRank = pranks.RankOf(playr)
		if cRank == -2 then
			MsgN("[ERROR] PlayeRanks: Error when trying to change " .. playr:GetName() .. "'s rank. Aborting...")
			return false
		elseif cRank == -1 then
			MsgN("[ERROR] PlayeRanks: Player " .. playr:GetName() .. " was not found in the database.")
			return false
		else
			local query = sql.Query("UPDATE playerank SET rank=" .. tostring(rank) .. " WHERE steam=\'" .. sql.SQLStr(playr:SteamID()) .. "\'")
			if query == false then
				MsgN("[ERROR] PlayeRanks: Failed to change " .. playr:GetName() .. "'s rank: " .. sql.LastError())
				return false
			else
				local prevRank = pranks.ranks[cRank].name
				local newRank = pranks.ranks[rank].name

				MsgN("[INFO] PlayeRanks: Changed " .. playr:GetName() .."'s rank from " .. prevRank .. " to " .. newRank)
				playr:ChatPrint("[PlayeRanks] Changed your rank from " .. prevRank .. " to " .. newRank)
				playr:SetNetworkedNumber("rank", rank)

				return true
			end
		end
	end

	-- Adds a player to the database and set his rank to the default rank (see pr_default_rank)
	-- playr: The target player (Player)
	-- Returns wether there was an error or not (Boolean)
	function pranks.AddPlayer(playr)
		local pExist = pranks.RankOf(playr)

		if pExist >= 0 then
			MsgN("[ERROR] PlayeRanks: Player " .. playr:GetName() .. " already known in the database !")
			return false
		elseif pExist == -2 then
			MsgN("[ERROR] PlayeRanks: Error when trying to know if " .. playr:GetName() .. " is already in the database !")
			return false
		end

		local query = sql.Query("INSERT INTO playerank (steam, rank) VALUES (\'" .. sql.SQLStr(playr:SteamID()) .. "\', " .. tostring(GetConVar("pr_default_rank"):GetInt()) .. ")")
		if query == false then
			MsgN("[ERROR] PlayeRanks: Failed to add player " .. playr:GetName() .. " to database: " .. sql.LastError())
			return false
		end

		MsgN("[INFO] PlayeRanks: Player " .. playr:GetName() .. " added !")
		return true
	end

	-- Update rank database an send it to (a) player(s)
	-- playr: Target wich will receive the updated ranks table (if nil broadcast it to all players)
	function pranks.Update(playr)
		net.Start("PlayeRanks")
		net.WriteUInt(table.Count(pranks.ranks), 5) -- Max 15 Ranks

		for _, r in pairs(pranks.ranks) do
			net.WriteString(r.name)
			net.WriteUInt(r.colorRed, 8)
			net.WriteUInt(r.colorGreen, 8)
			net.WriteUInt(r.colorBlue, 8)
		end

		if !playr then
			net.Broadcast()
		else
			net.Send(playr)
		end
	end

	-- Check if a rank has a right
	-- rank: The rank id (Integer)
	-- right: The right to be checked (String)
	-- Returns true if it has, else false (Boolean)
	function pranks.CheckRankRight(rank, right)
		if !pranks.ranks[rank] then return false end
		if pranks.ranks[rank].rights == "*" then return true end
		
		local parse = string.Explode(";", pranks.ranks[rank].rights)
		local rightDat = string.Explode(".", right)

		for _, r in pairs(parse) do
			if r == right then
				return true
			else
				local parseDat = string.Explode(".", r)
				local depth = 1
				local ok = true

				if table.Count(parseDat) > table.Count(rightDat) then
					ok = false
				else
					while parseDat[depth] and rightDat[depth] do
						if parseDat[depth] == "*" then
							return true
						elseif parseDat[depth] != rightDat[depth] then
							ok = false
							break
						end

						depth = depth + 1
					end
				end

				if ok then
					return true
				end
			end
		end

		return false
	end

	-- Check if a player has a right
	-- playr: The player (Player)
	-- right: The right to be checked (String)
	-- Returns true if it has, else false (Boolean)
	function pranks.CheckPlayerRight(playr, right)
		local rank = pranks.RankOf(playr)
		if rank < 1 then return false end
		
		return pranks.CheckRankRight(rank, right)
	end

	-- Get a rank id by his name
	-- name: Rank name (String)
	-- Returns -1 if the rank was not found, else the ID of the rank (Integer)
	function pranks.RankByName(name)
		for _, v in pairs(pranks.ranks) do
			if string.lower(v.name) == string.lower(name) then
				return _
			end
		end

		return -1
	end

	-- [UTIL] Find a player by his name
	-- name: Target player's name (String)
	-- Returns the player if found, else nil (Player)
	function pranks.Player(name)
		for _, v in pairs(player.GetAll()) do
			if string.find(v:GetName():lower(), name:lower()) then
				return v
			end
		end

		return nil
	end

	-- [HOOK] When a player connects, it "network" his rank et send him the ranks table
	hook.Add("PlayerInitialSpawn", "PRanksNetworkVar", function(playr)
		local cRank = pranks.RankOf(playr)

		if cRank == -2 then
			MsgN("[ERROR] PlayeRanks: An error occured when trying to get " .. playr:GetName() .. "'s rank.")

			if GetConVar("pr_kickerror"):GetInt() == 1 then
				playr:Kick("[PlayeRanks] Internal server error. Try again later.")
				return
			end
		elseif cRank == -1 then
			MsgN("[INFO] PlayeRanks: Adding new player " .. playr:GetName() .. " to database...")

			if !AddPlayer(playr) and GetConVar("pr_kickerror"):GetInt() == 1 then
				playr:Kick("[PlayeRanks] Internal server error. Try again later.")
				return
			end
			
			playr:SetNetworkedNumber("rank", GetConVar("pr_default_rank"):GetInt())
		else
			playr:SetNetworkedNumber("rank", cRank)
		end

		-- Send ranks list to player
		pranks.Update(playr)
	end)

	-- [HOOK] Parse the !rank commands
	hook.Add("PlayerSay", "RankHook", function(playr, text, toteam, dead)
		if !playr or !text or toteam then return end
		
		local args = string.Explode(" ", text)
		if args[1] and args[1] == "!rank" and args[2] and args[3] then
			if !pranks.CheckPlayerRight(playr, "change") then
				playr:ChatPrint("[PlayeRanks] You do not have the right to do that !")
				return
			end
			
			local tRank = pranks.RankByName(args[3])
			if tRank <= 0 then
				playr:ChatPrint("[PlayeRanks] Rank " .. args[3] .. " not found !")
				return
			end

			local target = pranks.Player(args[2])
			if !target or !target:IsValid() or !target:IsPlayer() then
				playr:ChatPrint("[PlayeRanks] Player " .. args[2] .. " not found !")
				return
			end

			local bRank = pranks.RankOf(target)
			if bRank == -2 then
				playr:ChatPrint("[PlayeRanks] Error when trying to change " .. target:GetName() .. "'s rank !")
				return
			elseif bRank == -1 then
				playr:ChatPrint("[PlayeRanks] Player " .. target:GetName() .. " is not known in the database !")
				return
			end

			pranks.SetRank(target, tRank)
			playr:ChatPrint("[PlayeRanks] Changed " .. target:GetName() .. "'s rank from " .. pranks.ranks[bRank].name .. " to " .. pranks.ranks[tRank].name .. " !")
		end
	end)
end

MsgN("[INFO] PlayeRanks: Initialization ended server-side.")
